// Copyright 2000-2006, FreeHEP
package org.freehep.graphics2d;

import java.awt.*;
import java.awt.font.TextLayout;
import java.awt.geom.*;
import java.text.AttributedCharacterIterator;
import java.util.Properties;

import org.freehep.util.UserProperties;

/**
 * This class implements all conversions from integer to double as well as a few
 * other convenience functions. It also handles the different drawSymbol and
 * fillSymbol methods and print colors. The drawing of framed strings is broken
 * down to lower level methods.
 * 
 * @author Simon Fischer
 * @author Mark Donszelmann
 * @author Steffen Greiffenberg
 * @version $Id: AbstractVectorGraphics.java,v 1.4 2009/08/17 21:44:44 murkle
 *          Exp $
 */
public abstract class AbstractVectorGraphics extends VectorGraphics {

  private UserProperties properties;

  private String creator;

  private boolean isDeviceIndependent;

  private final SymbolShape cachedShape;

  private int colorMode;

  private Color backgroundColor;

  private Color currentColor;

  private Paint currentPaint;

  private Font currentFont;

  private static final double bias = 0.5;

  public AbstractVectorGraphics() {
    properties = new UserProperties();
    creator = "FreeHEP Graphics2D Driver";
    isDeviceIndependent = false;
    cachedShape = new SymbolShape();
    colorMode = PrintColor.COLOR;

    // all of these have to be set in the subclasses
    currentFont = null;
    backgroundColor = null;
    currentColor = null;
    currentPaint = null;
  }

  protected AbstractVectorGraphics(AbstractVectorGraphics graphics) {
    super();
    properties = graphics.properties;
    creator = graphics.creator;
    isDeviceIndependent = graphics.isDeviceIndependent;
    cachedShape = graphics.cachedShape;

    backgroundColor = graphics.backgroundColor;
    currentColor = graphics.currentColor;
    currentPaint = graphics.currentPaint;
    colorMode = graphics.colorMode;
    currentFont = graphics.currentFont;
  }

  @Override
  public void clearRect(int x, int y, int width, int height) {
    clearRect(x + bias, y + bias, width, height);
  }

  /**
   * Creates a polyline/polygon shape from a set of points. Needs to be defined
   * in subclass because its implementations could be device specific
   * 
   * @param xPoints
   *          X coordinates of the polyline.
   * @param yPoints
   *          Y coordinates of the polyline.
   * @param nPoints
   *          number of points of the polyline.
   * @param close
   *          is shape closed
   */
  protected abstract Shape createShape(double[] xPoints, double[] yPoints,
      int nPoints, boolean close);

  /**
   * Creates a polyline/polygon shape from a set of points. Needs a bias!
   * 
   * @param xPoints
   *          X coordinates of the polyline.
   * @param yPoints
   *          Y coordinates of the polyline.
   * @param nPoints
   *          number of points of the polyline.
   * @param close
   *          is shape closed
   */
  private Shape createShape(int[] xPoints, int[] yPoints, int nPoints,
      boolean close, boolean biased) {

    float offset = biased ? (float) bias : 0.0f;
    GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
    if (nPoints > 0) {
      path.moveTo(xPoints[0] + offset, yPoints[0] + offset);
      int lastX = xPoints[0];
      int lastY = yPoints[0];
      if (close && Math.abs(xPoints[nPoints - 1] - lastX) < 1
          && Math.abs(yPoints[nPoints - 1] - lastY) < 1)
        nPoints--;
      for (int i = 1; i < nPoints; i++)
        if (Math.abs(xPoints[i] - lastX) > 1
            || Math.abs(yPoints[i] - lastY) > 1) {
          path.lineTo(xPoints[i] + offset, yPoints[i] + offset);
          lastX = xPoints[i];
          lastY = yPoints[i];
        }
      if (close)
        path.closePath();
    }
    return path;
  }

  @Override
  public void drawArc(double x, double y, double width, double height,
      double startAngle, double arcAngle) {
    draw(new Arc2D.Double(x, y, width, height, startAngle, arcAngle, Arc2D.OPEN));
  }

  @Override
  public void drawArc(int x, int y, int width, int height, int startAngle,
      int arcAngle) {
    drawArc(x + bias, y + bias, width, height, startAngle, arcAngle);
  }

  /**
   * Draws frame and banner for a TextLayout, which is used for calculation auf
   * ajustment
   * 
   * @param tl
   *          TextLayout for frame calculation
   * @param x
   *          coordinate to draw string
   * @param y
   *          coordinate to draw string
   * @param horizontal
   *          alignment of the text
   * @param vertical
   *          alignment of the text
   * @param framed
   *          true if text is surrounded by a frame
   * @param frameColor
   *          color of the frame
   * @param frameWidth
   *          witdh of the frame
   * @param banner
   *          true if the frame is filled by a banner
   * @param bannerColor
   *          color of the banner
   * @return Offset for the string inside the frame
   */
  private Point2D drawFrameAndBanner(TextLayout tl, double x, double y,
      int horizontal, int vertical, boolean framed, Color frameColor,
      double frameWidth, boolean banner, Color bannerColor) {

    // calculate string bounds for alignment
    Rectangle2D bounds = tl.getBounds();

    // calculate real bounds
    bounds.setRect(bounds.getX(), bounds.getY(),
    // care for Italic fonts too
        Math.max(tl.getAdvance(), bounds.getWidth()), bounds.getHeight());

    // add x and y
    AffineTransform at = AffineTransform.getTranslateInstance(x, y);

    // horizontal alignment
    if (horizontal == TEXT_RIGHT)
      at.translate(-bounds.getWidth(), 0);
    else if (horizontal == TEXT_CENTER)
      at.translate(-bounds.getWidth() / 2, 0);

    // vertical alignment
    if (vertical == TEXT_BASELINE) {
      // no translation needed
    } else if (vertical == TEXT_TOP)
      at.translate(0, -bounds.getY());
    else if (vertical == TEXT_CENTER)
      // the following adds supersript ascent too,
      // so it does not work
      // at.translate(0, tl.getAscent() / 2);
      // this is nearly the same
      at.translate(0, tl.getDescent());
    else if (vertical == TEXT_BOTTOM)
      at.translate(0, -bounds.getHeight() - bounds.getY());

    // transform the bounds
    bounds = at.createTransformedShape(bounds).getBounds2D();
    // create the result with the same transformation
    Point2D result = at.transform(new Point2D.Double(0, 0),
        new Point2D.Double());

    // space between string and border
    double adjustment = getFont().getSize2D() * 2 / 10;

    // add the adjustment
    bounds
        .setRect(bounds.getX() - adjustment, bounds.getY() - adjustment, bounds
            .getWidth()
            + 2 * adjustment, bounds.getHeight() + 2 * adjustment);

    if (banner) {
      Paint paint = getPaint();
      setColor(bannerColor);
      fill(bounds);
      setPaint(paint);
    }
    if (framed) {
      Paint paint = getPaint();
      Stroke stroke = getStroke();
      setColor(frameColor);
      setLineWidth(frameWidth);
      draw(bounds);
      setPaint(paint);
      setStroke(stroke);
    }

    return result;
  }

  @Override
  public void drawLine(double x1, double y1, double x2, double y2) {
    draw(new Line2D.Double(x1, y1, x2, y2));
  }

  @Override
  public void drawLine(int x1, int y1, int x2, int y2) {
    drawLine(x1 + bias, y1 + bias, x2 + bias, y2 + bias);
  }

  @Override
  public void drawOval(double x, double y, double width, double height) {
    draw(new Ellipse2D.Double(x, y, width, height));
  }

  @Override
  public void drawOval(int x, int y, int width, int height) {
    drawOval(x + bias, y + bias, width, height);
  }

  @Override
  public void drawPolygon(double[] xPoints, double[] yPoints, int nPoints) {
    draw(createShape(xPoints, yPoints, nPoints, true));
  }

  @Override
  public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) {
    draw(createShape(xPoints, yPoints, nPoints, true, true));
  }

  @Override
  public void drawPolyline(double[] xPoints, double[] yPoints, int nPoints) {
    draw(createShape(xPoints, yPoints, nPoints, false));
  }

  @Override
  public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) {
    draw(createShape(xPoints, yPoints, nPoints, false, true));
  }

  @Override
  public void drawRect(double x, double y, double width, double height) {
    draw(new Rectangle2D.Double(x, y, width, height));
  }

  @Override
  public void drawRect(int x, int y, int width, int height) {
    drawRect(x + bias, y + bias, width, height);
  }

  @Override
  public void drawRoundRect(double x, double y, double width, double height,
      double arcWidth, double arcHeight) {
    draw(new RoundRectangle2D.Double(x, y, width, height, arcWidth, arcHeight));
  }

  @Override
  public void drawRoundRect(int x, int y, int width, int height, int arcWidth,
      int arcHeight) {
    drawRoundRect(x + bias, y + bias, width, height, arcWidth, arcHeight);
  }

  @Override
  public void drawString(AttributedCharacterIterator iterator, int x, int y) {
    drawString(iterator, (float) x, (float) y);
  }

  @Override
  public void drawString(String str, double x, double y, int horizontal,
      int vertical) {
    drawString(str, x, y, horizontal, vertical, false, null, 0, false, null);
  }

  /**
   * Draws frame, banner and aligned text inside
   * 
   * @param str
   *          text to be drawn
   * @param x
   *          coordinate to draw string
   * @param y
   *          coordinate to draw string
   * @param horizontal
   *          alignment of the text
   * @param vertical
   *          alignment of the text
   * @param framed
   *          true if text is surrounded by a frame
   * @param frameColor
   *          color of the frame
   * @param frameWidth
   *          witdh of the frame
   * @param banner
   *          true if the frame is filled by a banner
   * @param bannerColor
   *          color of the banner
   */
  @Override
  public void drawString(String str, double x, double y, int horizontal,
      int vertical, boolean framed, Color frameColor, double frameWidth,
      boolean banner, Color bannerColor) {

    // change the x offset for the next drawing
    // FIXME: change y offset for vertical text
    TextLayout tl = new TextLayout(str, getFont().getAttributes(),
        getFontRenderContext());

    // draw the frame
    Point2D offset = drawFrameAndBanner(tl, x, y, horizontal, vertical, framed,
        frameColor, frameWidth, banner, bannerColor);

    // draw the string
    drawString(str, offset.getX(), offset.getY());
  }

  @Override
  public void drawString(String s, float x, float y) {
    drawString(s, (double) x, (double) y);
  }

  @Override
  public void drawString(String str, int x, int y) {
    drawString(str, (double) x, (double) y);
  }

  @Override
  public void drawString(TagString str, double x, double y) {
    drawString(str, x, y, TEXT_LEFT, TEXT_BASELINE);
  }

  @Override
  public void drawString(TagString str, double x, double y, int horizontal,
      int vertical) {
    drawString(str, x, y, horizontal, vertical, false, null, 0, false, null);
  }

  // ---------------------------------------------------------
  // -------------------- WRAPPER METHODS --------------------
  // -------------------- int -> double --------------------
  // needs a bias in some cases
  // ---------------------------------------------------------

  /**
   * Draws the tagged string parsed by a {@link TagHandler} and adds a border
   * specified by the parameters
   * 
   * @param str
   *          Tagged text to be drawn
   * @param x
   *          coordinate to draw string
   * @param y
   *          coordinate to draw string
   * @param horizontal
   *          alignment of the text
   * @param vertical
   *          alignment of the text
   * @param framed
   *          true if text is surrounded by a frame
   * @param frameColor
   *          color of the frame
   * @param frameWidth
   *          witdh of the frame
   * @param banner
   *          true if the frame is filled by a banner
   * @param bannerColor
   *          color of the banner
   */
  @Override
  public void drawString(TagString str, double x, double y, int horizontal,
      int vertical, boolean framed, Color frameColor, double frameWidth,
      boolean banner, Color bannerColor) {

    GenericTagHandler tagHandler = new GenericTagHandler(this);
    TextLayout tl = tagHandler.createTextLayout(str,
        getFont().getSize2D() / 7.5);

    // draw the frame
    Point2D offset = drawFrameAndBanner(tl, x, y, horizontal, vertical, framed,
        frameColor, frameWidth, banner, bannerColor);

    // FIXME: not quite clear why correction is needed
    // see {@link GenericTagHandler#superscriptCorrection
    tagHandler.print(str, offset.getX(), offset.getY(),
        getFont().getSize2D() / 7.5);
  }

  @Override
  public void drawSymbol(double x, double y, double size, int symbol) {
    if (size <= 0)
      return;
    drawSymbol(this, x, y, size, symbol);
  }

  @Override
  public void drawSymbol(int x, int y, int size, int symbol) {
    drawSymbol((double) x, (double) y, (double) size, symbol);
  }

  protected void drawSymbol(VectorGraphics g, double x, double y, double size,
      int symbol) {
    switch (symbol) {
      case SYMBOL_VLINE :
      case SYMBOL_STAR :
      case SYMBOL_HLINE :
      case SYMBOL_PLUS :
      case SYMBOL_CROSS :
      case SYMBOL_BOX :
      case SYMBOL_UP_TRIANGLE :
      case SYMBOL_DN_TRIANGLE :
      case SYMBOL_DIAMOND :
        cachedShape.create(symbol, x, y, size);
        g.draw(cachedShape);
        break;

      case SYMBOL_CIRCLE : {
        double diameter = Math.max(1, size);
        diameter += diameter % 2;
        g.drawOval(x - diameter / 2, y - diameter / 2, diameter, diameter);
        break;
      }
    }
  }

  @Override
  public void fillAndDraw(Shape s, Color fillColor) {
    Color color = getColor();
    setColor(fillColor);
    fill(s);
    setColor(color);
    draw(s);
  }

  @Override
  public void fillAndDrawSymbol(double x, double y, double size, int symbol,
      Color fillColor) {
    Color color = getColor();
    setColor(fillColor);
    fillSymbol(x, y, size, symbol);
    setColor(color);
    drawSymbol(x, y, size, symbol);
  }

  @Override
  public void fillAndDrawSymbol(int x, int y, int size, int symbol,
      Color fillColor) {
    fillAndDrawSymbol((double) x, (double) y, (double) size, symbol, fillColor);
  }

  @Override
  public void fillArc(double x, double y, double width, double height,
      double startAngle, double arcAngle) {
    fill(new Arc2D.Double(x, y, width, height, startAngle, arcAngle, Arc2D.PIE));
  }

  @Override
  public void fillArc(int x, int y, int width, int height, int startAngle,
      int arcAngle) {
    fillArc((double) x, (double) y, (double) width, (double) height,
        (double) startAngle, (double) arcAngle);
  }

  @Override
  public void fillOval(double x, double y, double width, double height) {
    fill(new Ellipse2D.Double(x, y, width, height));
  }

  @Override
  public void fillOval(int x, int y, int width, int height) {
    fillOval((double) x, (double) y, (double) width, (double) height);
  }

  @Override
  public void fillPolygon(double[] xPoints, double[] yPoints, int nPoints) {
    fill(createShape(xPoints, yPoints, nPoints, true));
  }

  @Override
  public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) {
    fill(createShape(xPoints, yPoints, nPoints, true, false));
  }

  @Override
  public void fillRect(double x, double y, double width, double height) {
    fill(new Rectangle2D.Double(x, y, width, height));
  }

  @Override
  public void fillRect(int x, int y, int width, int height) {
    fillRect((double) x, (double) y, (double) width, (double) height);
  }

  @Override
  public void fillRoundRect(double x, double y, double width, double height,
      double arcWidth, double arcHeight) {
    fill(new RoundRectangle2D.Double(x, y, width, height, arcWidth, arcHeight));
  }

  @Override
  public void fillRoundRect(int x, int y, int width, int height, int arcWidth,
      int arcHeight) {
    fillRoundRect((double) x, (double) y, (double) width, (double) height,
        (double) arcWidth, (double) arcHeight);
  }

  @Override
  public void fillSymbol(double x, double y, double size, int symbol) {
    if (size <= 0)
      return;
    fillSymbol(this, x, y, size, symbol);
  }

  @Override
  public void fillSymbol(int x, int y, int size, int symbol) {
    fillSymbol((double) x, (double) y, (double) size, symbol);
  }

  protected void fillSymbol(VectorGraphics g, double x, double y, double size,
      int symbol) {
    switch (symbol) {
      case SYMBOL_VLINE :
      case SYMBOL_STAR :
      case SYMBOL_HLINE :
      case SYMBOL_PLUS :
      case SYMBOL_CROSS :
        cachedShape.create(symbol, x, y, size);
        g.draw(cachedShape);
        break;

      case SYMBOL_BOX :
      case SYMBOL_UP_TRIANGLE :
      case SYMBOL_DN_TRIANGLE :
      case SYMBOL_DIAMOND :
        cachedShape.create(symbol, x, y, size);
        g.fill(cachedShape);
        break;

      case SYMBOL_CIRCLE : {
        double diameter = Math.max(1, size);
        diameter += diameter % 2;
        g.fillOval(x - diameter / 2, y - diameter / 2, diameter, diameter);
        break;
      }
    }
  }

  // ------------------ other wrapper methods ----------------

  /**
   * Gets the background color.
   * 
   * @return background color
   */
  @Override
  public Color getBackground() {
    return backgroundColor;
  }

  /**
   * Gets the current color.
   * 
   * @return the current color
   */
  @Override
  public Color getColor() {
    return currentColor;
  }

  /* 8.2. paint/color */
  @Override
  public int getColorMode() {
    return colorMode;
  }

  @Override
  public String getCreator() {
    return creator;
  }

  /**
   * Gets the current font.
   * 
   * @return current font
   */
  @Override
  public Font getFont() {
    return currentFont;
  }

  /**
   * Gets the current paint.
   * 
   * @return paint current paint
   */
  @Override
  public Paint getPaint() {
    return currentPaint;
  }

  /**
   * Returns a printColor created from the original printColor, based on the
   * ColorMode. If you run getPrintColor on it again you still get the same
   * color, so that it does not matter if you convert it more than once.
   */
  protected Color getPrintColor(Color color) {
    // shortcut if mode is COLOR for speed
    if (colorMode == PrintColor.COLOR)
      return color;

    // otherwise...
    PrintColor printColor = PrintColor.createPrintColor(color);
    return printColor.getColor(colorMode);
  }

  @Override
  protected Properties getProperties() {
    return properties;
  }

  @Override
  public String getProperty(String key) {
    return properties.getProperty(key);
  }

  @Override
  public Color getPropertyColor(String key) {
    return properties.getPropertyColor(key);
  }

  @Override
  public Dimension getPropertyDimension(String key) {
    return properties.getPropertyDimension(key);
  }

  @Override
  public double getPropertyDouble(String key) {
    return properties.getPropertyDouble(key);
  }

  public Insets getPropertyInsets(String key) {
    return properties.getPropertyInsets(key);
  }

  @Override
  public int getPropertyInt(String key) {
    return properties.getPropertyInt(key);
  }

  @Override
  public Rectangle getPropertyRectangle(String key) {
    return properties.getPropertyRectangle(key);
  }

  @Override
  protected void initProperties(Properties defaults) {
    properties = new UserProperties();
    properties.setProperties(defaults);
  }

  @Override
  public boolean isDeviceIndependent() {
    return isDeviceIndependent;
  }

  @Override
  public boolean isProperty(String key) {
    return properties.isProperty(key);
  }

  @Override
  public void rotate(double theta, double x, double y) {
    translate(x, y);
    rotate(theta);
    translate(-x, -y);
  }

  /**
   * Sets the background color.
   * 
   * @param color
   *          background color to be set
   */
  @Override
  public void setBackground(Color color) {
    backgroundColor = color;
  }

  /**
   * Sets the current color and the current paint. Calls writePaint(Color).
   * 
   * @param color
   *          to be set
   */
  @Override
  public void setColor(Color color) {
    if (color == null)
      return;

    currentColor = color;
    currentPaint = color;
  }

  @Override
  public void setColorMode(int colorMode) {
    this.colorMode = colorMode;
  }

  @Override
  public void setCreator(String creator) {
    if (creator != null)
      this.creator = creator;
  }

  @Override
  public void setDeviceIndependent(boolean isDeviceIndependent) {
    this.isDeviceIndependent = isDeviceIndependent;
  }

  /**
   * Sets the current font.
   * 
   * @param font
   *          to be set
   */
  @Override
  public void setFont(Font font) {
    if (font == null)
      return;

    // FIXME: maybe add delayed setting
    currentFont = font;
  }

  @Override
  public void setLineWidth(double width) {
    Stroke stroke = getStroke();
    if (stroke instanceof BasicStroke) {
      BasicStroke cs = (BasicStroke) stroke;
      if (cs.getLineWidth() != width) {
        stroke = new BasicStroke((float) width, cs.getEndCap(), cs
            .getLineJoin(), cs.getMiterLimit(), cs.getDashArray(), cs
            .getDashPhase());
        setStroke(stroke);
      }
    } else {
      stroke = new BasicStroke((float) width);
      setStroke(stroke);
    }
  }

  /*--------------------------------------------------------------------------------
   | 8.1. stroke/linewidth
   *--------------------------------------------------------------------------------*/
  @Override
  public void setLineWidth(int width) {
    setLineWidth((double) width);
  }

  /**
   * Sets the current paint.
   * 
   * @param paint
   *          to be set
   */
  @Override
  public void setPaint(Paint paint) {
    if (paint == null)
      return;

    if (!(paint instanceof Color))
      currentColor = null;
    currentPaint = paint;
  }

  @Override
  public void setProperties(Properties newProperties) {
    if (newProperties == null)
      return;
    properties.setProperties(newProperties);
  }

  @Override
  public void translate(int x, int y) {
    translate((double) x, (double) y);
  }
}
